<?php
/**
 * Created by PhpStorm.
  * User: longli
 * VX: isa1589518286
 * Date: 2020/07/18
 * Time: 23:24
 * @link http://www.lmterp.cn
 */

namespace app\common\library;

use Exception;
use RuntimeException;

class ParseCrontab
{
    public function __construct($strCron = '')
    {
        $this->strCron = trim($strCron);
    }

    /**
     * 检查某时间($time)是否符合某个corntab时间计划($strCron)
     * @param int $time 时间戳
     * @return array|bool|string 出错返回string（错误信息）
     * @date 2020/07/18
     * @author longli
     */
    protected function check($time)
    {
        $formatTime = $this->formatTimestamp($time);
        $formatCron = $this->formatCrontab();
        if(!is_array($formatCron)) return $formatCron;
        return $this->formatCheck($formatTime, $formatCron);
    }

    /**
     * 使用格式化的数据检查某时间($formatTime)是否符合某个corntab时间计划($formatCron)
     * @param array $formatTime $this->formatTimestamp()格式化时间戳得到
     * @param array $formatCron $this->formatCrontab()格式化的时间计划
     * @return bool
     * @date 2020/07/18
     * @author longli
     */
    protected function formatCheck(array $formatTime, array $formatCron)
    {
        return (!$formatCron[0] || in_array($formatTime[0], $formatCron[0]))
            && (!$formatCron[1] || in_array($formatTime[1], $formatCron[1]))
            && (!$formatCron[2] || in_array($formatTime[2], $formatCron[2]))
            && (!$formatCron[3] || in_array($formatTime[3], $formatCron[3]))
            && (!$formatCron[4] || in_array($formatTime[4], $formatCron[4]));
    }

    /**
     * 格式化时间戳，以便比较
     * @param int $time 时间戳
     * @return array
     * @date 2020/07/18
     * @author longli
     */
    protected function formatTimestamp($time)
    {
        return explode('-', date('i-G-j-n-w', $time));
    }

    /**
     * 格式化crontab时间设置字符串,用于比较
     * @return array|string 正确返回数组，出错返回字符串（错误信息）
     * @date 2020/07/18
     * @author longli
     */
    public function formatCrontab()
    {
        //格式检查
        $strCron = $this->getStrCron();
        $regex = '#^((\*(/\d+)?|((\d+(-\d+)?)(?3)?)(,(?4))*))( (?2)){5}$#';
        if(!preg_match($regex, $strCron)) return '格式错误';
        try {
            //分别解析秒、分、时、日、月、周
            $cron = [];
            $parts = explode(' ', $strCron);
            $cron[0] = $this->parseCronPart($parts[0], 0, 60);//秒
            $cron[1] = $this->parseCronPart($parts[1], 0, 59);//分
            $cron[2] = $this->parseCronPart($parts[2], 0, 59);//时
            $cron[3] = $this->parseCronPart($parts[3], 1, 31);//日
            $cron[4] = $this->parseCronPart($parts[4], 1, 12);//月
            $cron[5] = $this->parseCronPart($parts[5], 0, 6);//周（0周日）
        } catch(Exception $e)
        {
            return $e->getMessage();
        }
        return $cron;
    }

    /**
     * 解析crontab时间计划里一个部分(秒、分、时、日、月、周)的取值列表
     * @param string $part 时间计划里的一个部分，被空格分隔后的一个部分
     * @param int $fMin 此部分的最小取值
     * @param int $fMax 此部分的最大取值
     * @return array 若为空数组则表示可任意取值
     * @throws RuntimeException
     * @date 2020/07/18
     * @author longli
     */
    protected function parseCronPart($part, $fMin, $fMax)
    {
        $list = [];
        //处理"," -- 列表
        if(false !== strpos($part, ','))
        {
            $arr = explode(',', $part);
            foreach($arr as $v)
            {
                $tmp = $this->parseCronPart($v, $fMin, $fMax);
                $list = array_merge($list, $tmp);
            }
            return $list;
        }

        //处理"/" -- 间隔
        $tmp = explode('/', $part);
        $part = $tmp[0];
        $step = isset($tmp[1]) ? $tmp[1] : 1;
        //处理"-" -- 范围
        if(false !== strpos($part, '-'))
        {
            list($min, $max) = explode('-', $part);
            if($min > $max) throw new RuntimeException('使用"-"设置范围时，左不能大于右');
        } elseif($part == '*')
        {
            $min = $fMin;
            $max = $fMax;
        }
        else
        {
            //数字
            $min = $max = $part;
        }

        //空数组表示可以任意值
        if($min == $fMin && $max == $fMax && $step == 1) return $list;

        //越界判断
        if($min < $fMin || $max > $fMax)
            throw new RuntimeException('数值越界。应该：分0-59，时0-59，日1-31，月1-12，周0-6');

        return $max - $min > $step
            ? range($min, $max, $step)
            : [intval($min)];
    }

    /**
     * 检查计划任务是否可执行
     * @return bool
     * @date 2020/07/18
     * @author longli
     */
    public function isExecute()
    {
        $time = $this->formatCrontab();
        if(is_string($time)) throw new RuntimeException($time);
        $flag = true;
        // 设置对应的格式化时间
        $format = ['s', 'i', 'H', 'd', 'm', 'w'];
        $oldk = null;
        // 第一次检查是否全部为 *
        foreach($format as $key => $value)
        {
            $t = intval(date($value));
            if(!empty($time[$key]))
            {
                $oldk = $key;
                if(!in_array($t, $time[$key]))
                {
                    $flag = false;
                    break;
                }
            }
        }
        // 第二次检查，如果有不为 * 的，检查他之前的时间是否都为 0，或指定的时间
        if($flag && $oldk !== null)
        {
            for($i = 0; $i < $oldk; $i++)
            {
                $t = intval(date($format[$i]));
                // echo "{$format[$i]} => $t";
                if((!empty($time[$i]) && !in_array($t, $time[$i])) || (empty($time[$i]) && $t > 0))
                {
                    $flag = false;
                    break;
                }
            }
        }
        return $flag;
    }

    /**
     * 需要执行的目标方法
     * @param callable|object $target
     * @date 2020/07/19
     * @author longli
     */
    public function execute($target = null)
    {
        if($target === null) $target = $this->getTarget();
        if(is_callable($target))
        {
            $target();
            return;
        }
        if(class_exists($target)) $target = new $target();
        if(is_object($target) && method_exists($target, 'execute')) $target->execute();
    }

    /**
     * 目标方法
     * @var string
     */
    protected $target;

    public function getTarget()
    {
        return $this->target;
    }

    public function setTarget($target)
    {
        $this->target = $target;
    }

    /**
     * crontab的时间计划字符串，如"* 15 3 * * *"
     * @var string
     */
    protected $strCron = '';

    public function getStrCron()
    {
        return $this->strCron;
    }

    public function setStrCron($strCron)
    {
        $this->strCron = trim($strCron);
    }
}